#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>

/* Los .h para redes... */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>

static void show_usage(void);
static char *parse_server(const char *url);
static int connect_to_server(const char *server_name);
static bool send_string(int socket, const char *str);
static bool send_request(int socket, const char *url);
static char *read_line(int socket);
static bool check_http_response(const char *response);
static bool get_response(int socket, const char *filename);

static void show_usage(void) {
	fprintf(stderr, "Uso: \n"
	                "     hget http://...\n"
	                "     hget --help\n\n");
}

#define PREFIX "http://"
#define HTTP_PORT 80

/*
 * Obtiene el server de una URL. Por ejemplo, si recibe como input
 * "http://www.famaf.unc.edu.ar/ofertaAcademica/carrerasDeGrado/lCsComp/index.html"
 * devuelve "www.famaf.unc.edu.ar"
 *
 * El llamador es el dueño de la memoria devuelta
 *
 * Precondición: url != NULL;
 *               url comienza con PREFIX
 * Postcondición: resultado != NULL
 *                url comienza con PREFIX++resultado
 *                resultado no contiene '/'
 *                resultado es la cadena más larga posible que cumple lo anterior
 */
static char *parse_server(const char *url) {
	char *result=NULL;
	const char *path=NULL;
	size_t L = strlen(PREFIX); /* Constante, para no calcular strlen 187 veces */

	/* Precondicion */
	assert(url != NULL);
	assert(strncmp(url, PREFIX, L) == 0);

	/* Buscamos la primera '/' */
	path = strchr(url + L, '/');
	if(path == NULL) {
		/* Nos dieron algo del estilo "http://servidor.com" */
		path = url + strlen(url); /* Apuntamos path al '\0*' final de la url */
	}

	result = calloc(path - (url + L) + 1, sizeof(char));
	assert(result != NULL);
	strncpy(result, url + L, path - (url + L));

	/* Postcondicion */
	assert(result != NULL);
	assert(strchr(result, '/') == NULL);
	assert(strncmp(url + L, result, strlen(result)) == 0);

	return result;
}

/*
 * Se conecta al servidor llamado `server_name'
 *
 * Precondición: server_name != NULL
 * Postcondición: result == -1 en caso de error;
 *                result == descriptor conectado en caso de éxito
 */
static int connect_to_server(const char *server_name) {
	int result = -1, connection=-1;
	struct sockaddr_in dest_addr;
	struct hostent *h;

	h = gethostbyname(server_name);
    if (h == NULL) {
        printf("Error al conectar con el servidor\n");
        return -1;
    }
	result = socket(PF_INET, SOCK_STREAM, 0);
    if (result == -1) {
        printf("Error al crear el Socket");
        return -1;
    }
	dest_addr.sin_family = AF_INET;
	dest_addr.sin_port = htons(80);
	dest_addr.sin_addr = *((struct in_addr *)h->h_addr_list[0]);
	connection = connect(result, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
    if (connection != 0) {
        printf("Error al intentar establecer la conexión\n");
        return -1;
    }
	printf("%s\n", server_name);
	return result;
}

/*
 * Envia por `socket' la cadena `str'
 * `str' es terminada en '\0'. El '\0' no se transmite.
 *
 * Devuelve true en caso de éxito
 */
static bool send_string(int socket, const char *str) {
	int status = 0;
	size_t L = 0;

	assert(socket >= 0);
	assert(str != NULL);

	L = strlen(str);

	status = send(socket, str, L, 0);
	if(status == -1) {
		perror("send");
		return false;
	} else if(status < (int)L) {
		fprintf(stderr, "Transmisión falló parcialmente: se enviaron %d de %d bytes\n",
		        status, L);
		return false;
	}
	return true;
}

#define RQ1 "GET " /* Prefijo de un pedido */
#define RQ2 " HTTP/1.0\r\n\r\n" /* Sufijo de un pedido */

/*
 * Envía por `socket' un pedido HTTP de la URL dada
 *
 * Precondición: socket es válido y está conectado
 *               url != NULL
 */
static bool send_request(int socket, const char *url) {
	bool status=false;

	assert(socket >= 0);
	assert(url != NULL);

	/* Enviar prefijo */
	status = send_string(socket, RQ1);
	/* Enviar URL */
	if(status) status = send_string(socket, url);
	/* Enviar sufijo */
	if(status) status = send_string(socket, RQ2);

	return status;
}

/* 
 * Lee un renglón del socket. El llamador es dueño del resultado
 *
 * Precondición: socket es válido y está conectado
 * Postcondición: result != NULL en caso de éxito
 *                result == NULL en caso de error
 */
static char *read_line(int socket) {
	char *result = NULL;
	char *aux = NULL;
	size_t allocated = 0;
	size_t read = 0;
	ssize_t status = 0;

	assert(socket >= 0);

	result = calloc(32, sizeof(char));
	if(result != NULL){
		allocated = 32;
		status = recv(socket, result+read, 1, 0); /* Leer de a un byte */
		while(result != NULL && status > 0 && result[read] != '\n') {
			read += 1;
			if(read == allocated) {
				/* No hay más lugar, agrandar */
				allocated *= 2;
				aux = result;
				result = realloc(aux, allocated);
			}
			if( result != NULL ){
				status = recv(socket, result+read, 1, 0); /* Y un byte más... */
			}else{
				free(aux);
			}
		}
		if( result != NULL ){
			result[read] = '\0';
			if(status < 0) {
				free(result); result = NULL;
				perror("read_line: recv");
			}
		}
	}
	return result;
}

#define HTTP_OK "200"

/*
 * Verifica que el resultado de la operación http sea "OK" 
 */
static bool check_http_response(const char *response) {
	const char *protocol = NULL;
	const char *code = NULL;

	assert(response != NULL);

	protocol = response;

	code = strchr(response, ' ');
	while(code != NULL && *code == ' ') code++;

	if(code == NULL) {
		fprintf(stderr, "Respuesta HTTP mal formada\n");
		return false;
	} else if(strncmp(code, HTTP_OK, strlen(HTTP_OK)) != 0) {
		fprintf(stderr, "El servidor indicó error: %s\n", code);
		return false;
	}

	return true;
}

#define BUFFER_SIZE 4096

static bool get_response(int socket, const char *filename) {
	FILE *output = NULL;
	char buffer[BUFFER_SIZE];
	ssize_t bytes_read = 0;
	size_t bytes_written = 0;
	bool result = false;
	char *line = NULL;

	assert(socket >= 0);
	assert(filename != NULL);

	/* Analizar encabezado */
	line = read_line(socket);
	if(line == NULL) return false;
	/* Line deberia ser algo del estilo "Protocolo estado info", por ej:
	 * HTTP/1.1 200 OK
	 */
	if(!check_http_response(line)) {
		free(line);
		return false;
	}
	free(line);
	/* Saltear el resto del encabezado */
	line = read_line(socket);
	while(line != NULL && line[0] != '\r') {
		free(line);
		line = read_line(socket);
	}
	if(line == NULL) {
		fprintf(stderr, "La respuesta termino prematuramente.\n");
		return false;
	}
	free(line);

	/* Leer datos */
	output = fopen(filename, "wb");
	if(output == NULL) {
		perror("fopen");
		return false;
	}

	/* Transferir ... */
	result = true;
	bytes_read = recv(socket, buffer, BUFFER_SIZE, 0);
	while(result && bytes_read > 0) {
		assert(bytes_read <= BUFFER_SIZE); /* Me garantiza recv... */
		bytes_written = fwrite(buffer, sizeof(char), bytes_read, output);

		if(bytes_written != (size_t)bytes_read) {
			fprintf(stderr, "No se pudieron escribir %d bytes en el archivo de salida.\n", bytes_read);
			result = false;
		} else {
			/* Leer más */
			bytes_read = recv(socket, buffer, BUFFER_SIZE, 0);
		}
	}

	/* Revisamos estado... */
	if(bytes_read < 0) {
		perror("recv");
		result = false;
	}
	/* Sino, bytes_read==0, es decir, se terminaron los datos sin error de socket. */

	fclose(output);
	return result;
}

int main(int argc, char **argv) {
	int socketfd = -1; /* Descriptor de conexión al servidor */
	char *url = NULL; /* Direccion solicitada */
	char *server = NULL; /* Servidor a consultar */
	bool status = false;

	/* Parseo de argumentos */
	if(argc <= 1) { /* Faltan argumentos */
		show_usage();
		exit(1);
	}
	if(strcmp(argv[1], "--help") == 0) { /* --help */
		show_usage();
		exit(0);
	}
	/* Validación */
	url = argv[1];
	if(strncmp(url, PREFIX, strlen(PREFIX)) != 0) {
		fprintf(stderr, "La dirección '%s' no comienza con '%s'\n",
		        url, PREFIX);
		exit(1);
	}

	/* Obtener server */
	server = parse_server(url);
	fprintf(stderr, "Contactando servidor '%s'...\n", server);

	/* Conectar...*/
	socketfd = connect_to_server(server);
	if(socketfd < 0) {
		fprintf(stderr, "Fallo al conectarse.\n");
	} else {
		/* Enviar pedido */
		fprintf(stderr, "Contactado. Enviando pedido de URL '%s'...\n", url);
		status = send_request(socketfd, url);
		if(!status) {
			fprintf(stderr, "Fallo al enviar pedido.\n");
		} else {
			fprintf(stderr, "Pedido enviado. Recibiendo respuesta...\n");
			status = get_response(socketfd, "download");
			if(!status) {
				fprintf(stderr, "Fallo al recibir respuesta.\n");
			} else {
				fprintf(stderr, "Recibida.\n");
			}
		}
		close(socketfd);
	}
	free(server); server = NULL;

	return 0;
}
